package org.dreamwork.injection;

import com.google.gson.Gson;
import org.apache.log4j.PropertyConfigurator;
import org.dreamwork.cai.tools.EnhancedConfiguration;
import org.dreamwork.cli.Argument;
import org.dreamwork.cli.ArgumentParser;
import org.dreamwork.config.PropertyConfiguration;
import org.dreamwork.injection.impl.ObjectContextScanner;
import org.dreamwork.injection.impl.SimpleObjectContext;
import org.dreamwork.util.IOUtil;
import org.dreamwork.util.StringUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;

/**
 * 对象托管容器的工厂
 *
 * <p>这个工厂类负责初始化容器，解析配置，启动扫描</p>
 * <pre>
 * package org.example.app;
 *
 * &#64;AInjectionContext
 * public class MyAppMain {
 *     public static void main (String[] args) throws Exception {
 *         ObjectContextFactory.start (MyAppMain.class, args);
 *     }
 * }
 * </pre>
 * 扫描器将自动扫描 {@code org.example.app} 包下的所有类，所有带有 <strong>托管标注</strong> 的类
 * 都将自动被配置到托管容器内。
 * <p>如果希望扫描其他包，使用 {@link AInjectionContext#scanPackages() scanPackages} 属性来添加额外的包扫描</p>
 * <p>在默认情况下，扫描器 <strong>不会</strong> 递归扫描，若希望递归扫描，设置 {@link AInjectionContext#recursive() recursive}
 * 属性的值为 {@code true} 来激活递归扫描
 * </p>
 *
 * <h2>受托管对象</h2>
 * {@link javax.annotation.Resource} 标注用来表示一个对象/资源是 <strong><i>受托管</i></strong> 的。 托管容器将自动
 * 配置和管理这些对象实例。当 {@link javax.annotation.Resource} 用来标注: <ul>
 * <li>一个类时，意味着这个类将被作为受托管资源配置到托管容器内</li>
 * <li>一个 {@code java getter} 时，意味着这个 getter 的返回值将被自动配置到容器内</li>
 * <li>一个 {@code java setter} 时，意味着从容器内获取对应的资源并作为 setter 的参数注入到对象内</li>
 * <li>一个 {@code 字段} 时，意味着从容器内获取对应的资源输入到这个字段</li>
 * </ul>
 * 每个被 {@link javax.annotation.Resource} 标注的类，<strong>最多有一个</strong>方法被<ul>
 * <li>{@link javax.annotation.PostConstruct} 标注，意味着这个类在被配置到容器后将自动调用。这个方法的
 * 签名必须是<pre>public void &lt;methodName&gt; ()</pre></li>
 * <li>{@link javax.annotation.PreDestroy} 标注，意味着这个类从容器内删除前将自动调用。这个方法的签名必须是
 * <pre>public void &lt;methodName&gt; ()</pre></li>
 * </ul>
 *
 * <h2>简单的容器生命周期</h2>
 * 当容器解决了所有的依赖注入后，会调用所有实现了 {@link IInjectResolvedProcessor} 接口
 * 的实例的 {@link IInjectResolvedProcessor#perform(IObjectContext) perform} 方法。如果希望
 * 多个实现的运行顺序，应当实现 {@link IInjectResolvedProcessor#getOrder() getOrder} 方法，容器将
 * 按照这个方法的返回值排序，并按顺序调用，例如：<pre>
 * package org.example.app;
 * &#64;Resource
 * public class MyProcessor implements IInjectResolvedProcessor {
 *     &#64;Override
 *     public void perform (IObjectContext ctx) throws Exception {
 *         for (String name : ctx.getAllBeanNames ()) {
 *             System.out.println (name);
 *         }
 *     }
 * }
 * </pre>
 */
public class ObjectContextFactory {
    /**
     * 对象托管容器的工厂方法
     *
     * @param type 被 {@link AInjectionContext} 标注的类，用于提供托管容器的配置信息
     * @param args 命令行参数
     * @return 托管容器的实例
     * @throws Exception 任何异常
     */
    public static IObjectContext start (Class<?> type, String... args) throws Exception {
        if (!type.isAnnotationPresent (AInjectionContext.class)) {
            throw new TypeNotPresentException (AInjectionContext.class.getName (), null);
        }
        return new ObjectContextFactory (type).createObjectContext (args);
    }

    private final Class<?> type;

    private ObjectContextFactory (Class<?> type) {
        this.type = type;
    }

    private IObjectContext createObjectContext (String... args) throws Exception {
        ClassLoader loader = type.getClassLoader ();

        // 解析和处理配置/参数
        PropertyConfiguration configuration = initConfiguration (loader, args);
        // 创建 SimpleObjectContext
        SimpleObjectContext root = new SimpleObjectContext ();
        // 注册全局配置
        root.register ("global-config", configuration);

        AInjectionContext ic = type.getAnnotation (AInjectionContext.class);
        List<String> packages = fillPackageNames (ic, loader);

        ClassScanner scanner = new ObjectContextScanner (root);
        scanner.scan (packages.toArray (new String[0]));

        Runtime.getRuntime ().addShutdownHook (new Thread (root::dispose));
        return root;
    }

    private List<String> fillPackageNames (AInjectionContext ic, ClassLoader loader) throws IOException {
        List<String> packages = new ArrayList<> ();
        String base = type.getPackage ().getName ();
        packages.add (base);
        if (ic.recursive ()) {
            fillPackageNames (loader, base, packages);
        }

        String[] array = ic.value ();
        if (array.length == 0) {
            array = ic.scanPackages ();
        }

        for (String packageName : array) {
            packages.add (packageName);
            if (ic.recursive ()) {
                fillPackageNames (loader, packageName, packages);
            }
        }
        return packages;
    }

    private void fillPackageNames (ClassLoader loader, String base, List<String> list) throws IOException {
        String path = base.replace ('.', '/');
        try (InputStream in = loader.getResourceAsStream (path)) {
            if (in != null) {
                BufferedReader reader = new BufferedReader (new InputStreamReader (in));
                String line;
                while ((line = reader.readLine ()) != null) {
                    if (!line.contains (".")) {
                        list.add (base + "." + line);

                        fillPackageNames (loader, base + "." + line, list);
                    }
                }
            }
        }
    }

    private InputStream findArgumentDefinition (ClassLoader loader, String json) throws IOException {
        List<String> list = new ArrayList<> ();
        if (!StringUtil.isEmpty (json)) {
            list.add (json);
        }
        list.addAll (Arrays.asList ("../conf/cli-arguments.json", "cli-arguments.json", "../cli-arguments.json"));
        InputStream in;
        for (String path : list) {
            in = loader.getResourceAsStream (path);
            if (in != null) {
                System.out.printf ("find argument definition -- class:%s%n", path);
                return in;
            }
        }
        for (String path : list) {
            File file = new File (path);
            if (file.exists () && file.isFile () && file.canRead ()) {
                System.out.printf ("found argument definition -- file:%s%n", file.getCanonicalPath ());
                return new FileInputStream (file);
            }
        }

        return null;
    }

    private PropertyConfiguration initConfiguration (ClassLoader loader, String... args) throws IOException {
        Map<String, Argument> map = new HashMap<> ();
        Gson g = new Gson ();

        // 加载缺省的参数定义
        load (loader, g, map);

        AInjectionContext ic = type.getAnnotation (AInjectionContext.class);
        // 查找参数描述的 json 文件
        InputStream in = findArgumentDefinition (loader, ic.argumentDefinition ());
        if (in != null) try {
            load (g, map, in);
        } finally {
            in.close ();
        }

        ArgumentParser parser = new ArgumentParser (new ArrayList<> (map.values ()));
        parser.parse (args);

        try {
            if (!parser.isArgPresent ("without-logs"))
                initLogger (loader, parser);
        } catch (Exception ex) {
            throw new RuntimeException (ex);
        }

        // now, we can use the logger
        Logger logger = LoggerFactory.getLogger (ObjectContextFactory.class);

        return mergeConfig (parser, logger);
    }

    private PropertyConfiguration mergeConfig (ArgumentParser parser, Logger logger) {
        try {
            Properties props = parseConfig (parser, logger);
            EnhancedConfiguration configuration = new EnhancedConfiguration (props);

            // patch extra config dir
            setDefaultValue (parser, configuration, "ext.conf.dir", 'e');

            Collection<Argument> ca = parser.getAllArguments ();
            for (Argument a : ca) {
                if (!StringUtil.isEmpty (a.propKey)) {
                    if (!StringUtil.isEmpty (a.shortOption))
                        setDefaultValue (parser, configuration, a.propKey, a.shortOption.charAt (0));
                    else if (!StringUtil.isEmpty (a.longOption))
                        setDefaultValue (parser, configuration, a.propKey, a.longOption);
                }
            }

            if (logger.isTraceEnabled ()) {
                logger.trace ("configurations load complete, trying to start application");
            }

            return configuration;
        } catch (Exception ex) {
            logger.warn (ex.getMessage (), ex);
            throw new RuntimeException (ex);
        }
    }

    private Properties parseConfig (ArgumentParser parser, Logger logger) throws IOException {
        if (logger.isTraceEnabled ()) {
            logger.trace ("parsing config file ...");
        }
        String config_file = null;
        if (parser.isArgPresent ('c')) {
            config_file = parser.getValue ('c');
        }
        if (StringUtil.isEmpty (config_file)) {
            AInjectionContext ic = type.getAnnotation (AInjectionContext.class);
            if (!StringUtil.isEmpty (ic.config ())) {
                String name = ic.config ();
                if (logger.isTraceEnabled ()) {
                    logger.trace ("ic.config = {}", name);
                }
                File file = new File (name);
                if (file.isFile () && file.canRead ()) {
                    config_file = ic.config ();
                } else if (logger.isTraceEnabled ()) {
                    logger.trace ("annotated config {} actually not exists!!!", name);
                }
            }

            if (StringUtil.isEmpty (config_file)) {
                for (String name : Arrays.asList ("../conf/object-context.conf", "object-context.conf")) {
                    File file = new File (name);
                    if (file.isFile () && file.canRead ()) {
                        config_file = name;
                        break;
                    }
                }
            }
        }

        if (StringUtil.isEmpty (config_file)) {
            config_file = parser.getDefaultValue ('c');
        }

        config_file = config_file.trim ();
        if (logger.isTraceEnabled ()) {
            logger.trace ("config file: {}", config_file);
        }
        File file;
        if (config_file.startsWith ("file:/") || config_file.startsWith ("/")) {
            file = new File (config_file);
        } else {
            file = new File (".", config_file);
        }

        Properties props = new Properties ();
        if (!file.exists ()) {
            logger.warn ("can't find config file: {}", config_file);
            logger.warn ("using default config.");
        } else {
            try (InputStream in = new FileInputStream (file)) {
                props.load (in);
            }

            if (logger.isTraceEnabled ()) {
                prettyPrint (props, logger);
            }
        }
        return props;
    }

    private void load (Gson g, Map<String, Argument> map, InputStream in) throws IOException {
        String content = new String (IOUtil.read (in), StandardCharsets.UTF_8);
        List<Argument> list = g.fromJson (content, Argument.AS_LIST);
        list.forEach (item -> {
            String key = item.shortOption;
            if (StringUtil.isEmpty (key)) {
                key = item.longOption;
            }
            if (!StringUtil.isEmpty (key)) {
                map.put (key, item);
            }
        });
    }

    private void load (ClassLoader loader, Gson g, Map<String, Argument> map) {
        try (InputStream in = loader.getResourceAsStream ("default-arguments.json")) {
            if (in != null) {
                load (g, map, in);
            }
        } catch (Exception ex) {
            throw new RuntimeException (ex);
        }
    }

    public void prettyPrint (Properties props, Logger logger) {
        logger.trace ("### global configuration ###");
        int length = 0;
        List<String> list = new ArrayList<> ();
        for (String key : props.stringPropertyNames ()) {
            list.add (key);
            if (key.length () > length) {
                length = key.length ();
            }
        }
        list.sort (String::compareTo);
        for (String key : list) {
            StringBuilder builder = new StringBuilder (key);
            if (key.length () < length) {
                int d = length - key.length ();
                for (int i = 0; i < d; i ++) {
                    builder.append (' ');
                }
            }
            builder.append (" : ").append (props.getProperty (key));
            logger.trace (builder.toString ());
        }
        logger.trace ("############################");
    }

    private void setDefaultValue (ArgumentParser parser, PropertyConfiguration configuration, String key, char argument) {
        if (parser.isArgPresent (argument)) {
            configuration.setRawProperty (key, parser.getValue (argument));
        }
        if (!configuration.contains (key)) {
            configuration.setRawProperty (key, parser.getDefaultValue (argument));
        }
    }

    private void setDefaultValue (ArgumentParser parser, PropertyConfiguration configuration, String key, String argument) {
        if (parser.isArgPresent (argument)) {
            configuration.setRawProperty (key, parser.getValue (argument));
        }
        if (!configuration.contains (key)) {
            configuration.setRawProperty (key, parser.getDefaultValue (argument));
        }
    }

/*
    private static void loadExtProperties (IConfiguration conf, String name) {
        String ext_dir = conf.getString ("ext.conf.dir");
        Path path = Paths.get (ext_dir, name + ".conf");
        if (Files.exists (path)) {
            try (InputStream in = Files.newInputStream (path, StandardOpenOption.READ)) {
                Properties props = new Properties ();
                props.load (in);
//                context.putIfAbsent (name, new PropertyConfiguration (props));
            } catch (IOException ex) {
                ex.printStackTrace ();
            }
        }
    }
*/

    private void initLogger (ClassLoader loader, ArgumentParser parser) throws IOException {
        String logLevel, logFile;
        if (parser.isArgPresent ('v')) {
            logLevel = "TRACE";
        } else if (parser.isArgPresent ("log-level")) {
            logLevel = parser.getValue ("log-level");
        } else {
            logLevel = parser.getDefaultValue ("log-level");
        }

        logFile = parser.getValue ("log-file");
        if (StringUtil.isEmpty (logFile)) {
            logFile = parser.getDefaultValue ("log-file");
        }
        File file = new File (logFile);
        File parent = file.getParentFile ();
        if (!parent.exists () && !parent.mkdirs ()) {
            throw new IOException ("Can't create dir: " + parent.getCanonicalPath ());
        }

        if ("TRACE".equalsIgnoreCase (logLevel)) {
            System.out.printf ("## log file: %s ##%n", file.getCanonicalFile ());
        }

        try (InputStream in = loader.getResourceAsStream ("internal-log4j.properties")) {
            Properties props = new Properties ();
            props.load (in);

            System.out.println ("### setting log level to " + logLevel + " ###");
            if ("trace".equalsIgnoreCase (logLevel)) {
                props.setProperty ("log4j.rootLogger", "INFO, stdout, FILE");
                props.setProperty ("log4j.appender.FILE.File", logFile);
                props.setProperty ("log4j.appender.FILE.Threshold", logLevel);
                props.setProperty ("log4j.logger.org.dreamwork", "trace");
            } else {
                props.setProperty ("log4j.rootLogger", logLevel + ", stdout, FILE");
                props.setProperty ("log4j.appender.FILE.File", logFile);
                props.setProperty ("log4j.appender.FILE.Threshold", logLevel);
            }

            if (parser.isArgPresent ("trace-prefix")) {
                String prefixes = parser.getValue ("trace-prefix");
                if (!StringUtil.isEmpty (prefixes)) {
                    String[] parts = prefixes.trim ().split (File.pathSeparator);
                    for (String prefix : parts) {
                        if ("trace".equalsIgnoreCase (logLevel)) {
                            System.out.printf ("#### setting %s log level to trace ####%n", prefix);
                        }
                        props.setProperty ("log4j.logger." + prefix, "trace");
                    }
                }
            }

            PropertyConfigurator.configure (props);
        }
    }
}
